// ==UserScript==
// @name         忒星boss直聘批量简历投递+自动发送自定义消息[忒星修复魔改版]
// @description  忒星boss直聘批量简历投递[忒星修复魔改版]
// @namespace    yongjiu
// @version      1.2.4
// @author       maple,Ocyss,忒星
// @license      Apache License 2.0
// @run-at       document-start
// @match        https://www.zhipin.com/*
// @connect      https://github.com/yongjiu8/boss_push
// @include      https://www.zhipin.com
// @require      https://unpkg.com/maple-lib@1.0.3/log.js
// @require      https://cdn.jsdelivr.net/npm/axios@1.1.2/dist/axios.min.js
// @require      https://cdn.jsdelivr.net/npm/js2wordcloud@1.1.12/dist/js2wordcloud.min.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addValueChangeListener
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_cookie
// @grant        GM_notification
// ==/UserScript==

"use strict";

let logger = Logger.log("info")

class BossBatchExp extends Error {
    constructor(msg) {
        super(msg);
        this.name = "BossBatchExp";
    }
}

class JobNotMatchExp extends BossBatchExp {
    constructor(msg) {
        super(msg);
        this.name = "JobNotMatchExp";
    }
}

class PublishLimitExp extends BossBatchExp {
    constructor(msg) {
        super(msg);
        this.name = "PublishLimitExp";
    }
}

class FetchJobDetailFailExp extends BossBatchExp {
    constructor(msg) {
        super(msg);
        this.name = "FetchJobDetailFailExp";
    }
}

class SendPublishExp extends BossBatchExp {
    constructor(msg) {
        super(msg);
        this.name = "SendPublishExp";
    }
}

class PublishStopExp extends BossBatchExp {
    constructor(msg) {
        super(msg);
        this.name = "PublishStopExp";
    }
}


class TampermonkeyApi {
    static CUR_CK = ""

    constructor() {
        // fix 还未创建对象时，CUR_CK为空字符串，创建完对象之后【如果没有配置，则为null】导致key前缀不一致
        TampermonkeyApi.CUR_CK = GM_getValue("ck_cur", "");
    }

    static GmSetValue(key, val) {
        return GM_setValue(TampermonkeyApi.CUR_CK + key, val);
    }

    static GmGetValue(key, defVal) {
        return GM_getValue(TampermonkeyApi.CUR_CK + key, defVal);
    }

    static GMXmlHttpRequest(options) {
        return GM_xmlhttpRequest(options)
    }

    static GmAddValueChangeListener(key, func) {
        return GM_addValueChangeListener(TampermonkeyApi.CUR_CK + key, func);
    }

    static GmNotification(content) {
        GM_notification({
            title: "Boss直聘批量投简历",
            image:
                "https://img.bosszhipin.com/beijin/mcs/banner/3e9d37e9effaa2b6daf43f3f03f7cb15cfcd208495d565ef66e7dff9f98764da.jpg",
            text: content,
            highlight: true, // 布尔值，是否突出显示发送通知的选项卡
            silent: true, // 布尔值，是否播放声音
            timeout: 10000, // 设置通知隐藏时间
            onclick: function () {
                console.log("点击了通知");
            },
            ondone() {
            }, // 在通知关闭（无论这是由超时还是单击触发）或突出显示选项卡时调用
        });
    }
}

class Tools {


    /**
     * 模糊匹配
     * @param arr
     * @param input
     * @param emptyStatus
     * @returns {boolean|*}
     */
    static fuzzyMatch(arr, input, emptyStatus) {
        if (arr.length === 0) {
            // 为空时直接返回指定的空状态
            return emptyStatus;
        }
        input = input.toLowerCase();
        let emptyEle = false;
        // 遍历数组中的每个元素
        for (let i = 0; i < arr.length; i++) {
            // 如果当前元素包含指定值，则返回 true
            let arrEleStr = arr[i].toLowerCase();
            if (arrEleStr.length === 0) {
                emptyEle = true;
                continue;
            }
            if (arrEleStr.includes(input) || input.includes(arrEleStr)) {
                return true;
            }
        }

        // 所有元素均为空元素【返回空状态】
        if (emptyEle) {
            return emptyStatus;
        }

        // 如果没有找到匹配的元素，则返回 false
        return false;
    }


    // 范围匹配
    static rangeMatch(rangeStr, input, by = 1) {
        if (!rangeStr) {
            return true;
        }
        // 匹配定义范围的正则表达式
        let reg = /^(\d+)(?:-(\d+))?$/;
        let match = rangeStr.match(reg);

        if (match) {
            let start = parseInt(match[1]) * by;
            let end = parseInt(match[2] || match[1]) * by;

            // 如果输入只有一个数字的情况
            if (/^\d+$/.test(input)) {
                let number = parseInt(input);
                return number >= start && number <= end;
            }

            // 如果输入有两个数字的情况
            let inputReg = /^(\d+)(?:-(\d+))?/;
            let inputMatch = input.match(inputReg);
            if (inputMatch) {
                let inputStart = parseInt(inputMatch[1]);
                let inputEnd = parseInt(inputMatch[2] || inputMatch[1]);
                return (
                    (inputStart >= start && inputStart <= end) ||
                    (inputEnd >= start && inputEnd <= end)
                );
            }
        }

        // 其他情况均视为不匹配
        return false;
    }

    /**
     * 语义匹配
     * @param configArr
     * @param content
     * @returns {boolean}
     */
    static semanticMatch(configArr, content) {
        for (let i = 0; i < configArr.length; i++) {
            if (!configArr[i]) {
                continue
            }
            let re = new RegExp("(?<!(不|无).{0,5})" + configArr[i] + "(?!系统|软件|工具|服务)");
            if (re.test(content)) {
                return configArr[i];
            }
        }
    }

    static bossIsActive(activeText) {
        return !(activeText.includes("月") || activeText.includes("年"));
    }

    static getRandomNumber(startMs, endMs) {
        return Math.floor(Math.random() * (endMs - startMs + 1)) + startMs;
    }

    static getCookieValue(key) {
        const cookies = document.cookie.split(';');
        for (const cookie of cookies) {
            const [cookieKey, cookieValue] = cookie.trim().split('=');
            if (cookieKey === key) {
                return decodeURIComponent(cookieValue);
            }
        }
        return null;
    }

    static parseURL(url) {
        const urlObj = new URL(url);
        const pathSegments = urlObj.pathname.split('/');
        const jobId = pathSegments[2].replace('.html', '');
        const lid = urlObj.searchParams.get('lid');
        const securityId = urlObj.searchParams.get('securityId');

        return {
            securityId,
            jobId,
            lid
        };
    }

    static queryString(baseURL, queryParams) {
        const queryString = Object.entries(queryParams)
            .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
            .join('&');

        return `${baseURL}?${queryString}`;
    }

}

class DOMApi {

    static createTag(tag, name, style) {
        let htmlTag = document.createElement(tag);
        if (name) {
            htmlTag.innerHTML = name;
        }
        if (style) {
            htmlTag.style.cssText = style;
        }
        return htmlTag;
    }

    static createInputTag(descName, valueStr) {
        const inputNameLabel = document.createElement("label");
        inputNameLabel.textContent = descName;
        const inputTag = document.createElement("input");
        inputTag.type = "text";
        inputNameLabel.appendChild(inputTag);
        if (valueStr) {
            inputTag.value = valueStr;
        }

        // 样式
        inputNameLabel.style.cssText = "display: inline-block; margin: 0px 10px; font-weight: bold; width: 200px;";
        inputTag.style.cssText = "margin-left: 2px; width: 100%; padding: 5px; border-radius: 5px; border: 1px solid rgb(204, 204, 204); box-sizing: border-box;";
        return inputNameLabel;
    }

    static getInputVal(inputLab) {
        return inputLab.querySelector("input").value
    }

    static eventListener(tag, eventType, func) {
        tag.addEventListener(eventType, func)
    }

    static delElement(name, loop = false, el = document) {
        let t = setInterval(() => {
            const element = el.querySelector(name)
            if (!element) {
                if (!loop) {
                    clearInterval(t)
                }
                return
            }
            element.remove()
            clearInterval(t)
        }, 1000)
    }

    static setElement(name, style, el = document) {
        const element = el.querySelector(name)
        if (element) {
            for (let atr in style) {
                element.style[atr] = style[atr]
            }
        }
    }
}


class OperationPanel {

    constructor(jobListHandler) {
        // button
        this.batchPushBtn = null
        this.activeSwitchBtn = null
        this.sendSelfGreetSwitchBtn = null
        this.headhunterSwitchBtn = null
        this.bossOnlineSwitchBtn = null

        // inputLab
        // 公司名包含输入框lab
        this.cnInInputLab = null
        // 公司名排除输入框lab
        this.cnExInputLab = null
        // job名称包含输入框lab
        this.jnInInputLab = null
        // job内容排除输入框lab
        this.jcExInputLab = null
        // 薪资范围输入框lab
        this.srInInputLab = null
        // 公司规模范围输入框lab
        this.csrInInputLab = null
        // 自定义招呼语lab
        this.selfGreetInputLab = null

        // 词云图
        this.worldCloudModal = null
        this.worldCloudState = false // false:标签 true:内容
        this.worldCloudAllBtn = null

        this.topTitle = null

        // boss活跃度检测
        this.bossActiveState = true;

        //hr在线检测
        this.bossOnlineState = true;

        // 发送自定义招呼语
        this.sendSelfGreet = false;

        // 猎头岗位检测
        this.headhunterState = true;

        // 文档说明
        this.docTextArr = [
            "!加油，相信自己😶‍🌫️",
            "1.批量投递：点击批量投递开始批量投简历，请先通过上方Boss的筛选功能筛选大致的范围，然后通过脚本的筛选进一步确认投递目标。",
            "2.生成Job词云图：获取当前页面的所有job详情，并进行分词权重分析；生成岗位热点词汇词云图；帮助分析简历匹配度",
            "3.保存配置：保持下方脚本筛选项，用于后续直接使用当前配置。",
            "4.过滤不活跃Boss：打开后会自动过滤掉最近未活跃的Boss发布的工作。以免浪费每天的100次机会。",
            "5.发送自定义招呼语：因为boss不支持将自定义的招呼语设置为默认招呼语。开启表示发送boss默认的招呼语后还会发送自定义招呼语",
            "6.过滤猎头岗位：打开后会自动过滤掉猎头发布的工作。猎头的岗位要求一般都非常高，实际投此类岗位是无意义的，以免浪费每天的100次机会。",
            "7.可以在网站管理中打开通知权限,当停止时会自动发送桌面端通知提醒。",
            "😏",
            "脚本筛选项介绍：",
            "公司名包含：投递工作的公司名一定包含在当前集合中，模糊匹配，多个使用逗号分割。这个一般不用，如果使用了也就代表只投这些公司的岗位。例子：【阿里,华为】",
            "排除公司名：投递工作的公司名一定不在当前集合中，也就是排除当前集合中的公司，模糊匹配，多个使用逗号分割。例子：【xxx外包】",
            "排除工作内容：会自动检测上文(不是,不,无需等关键字),下文(系统,工具),例子：【外包,上门,销售,驾照】，如果写着是'不是外包''销售系统'那也不会被排除",
            "Job名包含：投递工作的名称一定包含在当前集合中，模糊匹配，多个使用逗号分割。例如：【软件,Java,后端,服务端,开发,后台】",
            "薪资范围：投递工作的薪资范围一定在当前区间中，一定是区间，使用-连接范围。例如：【12-20】",
            "公司规模范围：投递工作的公司人员范围一定在当前区间中，一定是区间，使用-连接范围。例如：【500-20000000】",
            "自定义招呼语：编辑自定义招呼语，当【发送自定义招呼语】打开时，投递后发送boss默认的招呼语【建议关闭默认打招呼语，使用自定义招呼语】后还会发送自定义招呼语；使用&lt;br&gt; \\n 换行；例子：【你好\\n我...】",
            "<h3>作者失业，找到工作别忘了赞助支持一下哦，多谢</h3>",
            "<img src='https://www.teixing.com/img/pay1.jpg' height='300px'/><img src='https://www.teixing.com/img/pay2.png' height='300px' />"
        ];

        // 相关链接
        this.aboutLink = [
            [
                ["GreasyFork", "https://greasyfork.org/zh-CN/scripts?q=%E5%BF%92%E6%98%9Fboss%E7%9B%B4%E8%81%98%E6%89%B9%E9%87%8F%E7%AE%80%E5%8E%86%E6%8A%95%E9%80%92",],
                ["魔改作者：忒星", "https://github.com/yongjiu8"],
                ["原版作者：yangfeng20", "https://github.com/yangfeng20"],
                ["去GitHub点个star⭐", "https://github.com/yongjiu8/boss_push"],
            ]
        ]

        this.scriptConfig = new ScriptConfig()
        this.jobListHandler = jobListHandler;
    }


    init() {
        this.renderOperationPanel();
        this.registerEvent();
    }


    /**
     * 渲染操作面板
     */
    renderOperationPanel() {

        logger.info("操作面板开始初始化")
        // 1.创建操作按钮并添加到按钮容器中【以下绑定事件处理函数均采用箭头函数作为中转，避免this执行事件对象】
        let btnCssText = "display: inline-block;border-radius: 4px;background: #e5f8f8;color: #00a6a7; text-decoration: none;margin: 20px 20px 0px 20px;padding: 6px 12px;cursor: pointer";

        // 批量投递按钮
        let batchPushBtn = DOMApi.createTag("div", "批量投递", btnCssText);
        this.batchPushBtn = batchPushBtn
        DOMApi.eventListener(batchPushBtn, "click", () => {
            this.batchPushBtnHandler()
        })

        // 保存配置按钮
        let storeConfigBtn = DOMApi.createTag("div", "保存配置", btnCssText);
        DOMApi.eventListener(storeConfigBtn, "click", () => {
            this.storeConfigBtnHandler()
        })

        // 生成Job词云图按钮
        let generateImgBtn = DOMApi.createTag("div", "生成词云图", btnCssText);
        DOMApi.eventListener(generateImgBtn, "click", () => {
            this.worldCloudModal.style.display = "flex"
            this.refreshQuantity()
        })

        // 投递后发送自定义打招呼语句
        this.sendSelfGreetSwitchBtn = DOMApi.createTag("div", "发送自定义打招呼语句", btnCssText);
        DOMApi.eventListener(this.sendSelfGreetSwitchBtn, "click", () => {
            this.sendSelfGreetSwitchBtnHandler(!this.sendSelfGreet)
        })
        this.sendSelfGreetSwitchBtnHandler(TampermonkeyApi.GmGetValue(ScriptConfig.SEND_SELF_GREET_ENABLE, false))

        // 过滤不活跃boss按钮
        this.activeSwitchBtn = DOMApi.createTag("div", "活跃度过滤", btnCssText);
        DOMApi.eventListener(this.activeSwitchBtn, "click", () => {
            this.activeSwitchBtnHandler(!this.bossActiveState)
        })
        // 默认开启活跃校验
        this.activeSwitchBtnHandler(this.bossActiveState)


        // 过滤HR在线按钮
        this.bossOnlineSwitchBtn = DOMApi.createTag("div", "HR在线过滤", btnCssText);
        DOMApi.eventListener(this.bossOnlineSwitchBtn, "click", () => {
            this.bossOnlineSwitchBtnHandler(!this.bossOnlineState)
        })
        //默认开启HR在线
        this.bossOnlineSwitchBtnHandler(this.bossOnlineState)

        // 过滤猎头岗位
        this.headhunterSwitchBtn = DOMApi.createTag("div", "过滤猎头岗位", btnCssText);
        DOMApi.eventListener(this.headhunterSwitchBtn, "click", () => {
            this.sendHeadhunterSwitchBtnHandler(!this.headhunterState)
        })
        this.sendHeadhunterSwitchBtnHandler(TampermonkeyApi.GmGetValue(ScriptConfig.SEND_HEADHUNTER_ENABLE, true));


        // 2.创建筛选条件输入框并添加到input容器中
        this.cnInInputLab = DOMApi.createInputTag("公司名包含", this.scriptConfig.getCompanyNameInclude());
        this.cnExInputLab = DOMApi.createInputTag("公司名排除", this.scriptConfig.getCompanyNameExclude());
        this.jnInInputLab = DOMApi.createInputTag("工作名包含", this.scriptConfig.getJobNameInclude());
        this.jcExInputLab = DOMApi.createInputTag("工作内容排除", this.scriptConfig.getJobContentExclude());
        this.srInInputLab = DOMApi.createInputTag("薪资范围", this.scriptConfig.getSalaryRange());
        this.csrInInputLab = DOMApi.createInputTag("公司规模范围", this.scriptConfig.getCompanyScaleRange());
        this.selfGreetInputLab = DOMApi.createInputTag("自定义招呼语", this.scriptConfig.getSelfGreet());
        DOMApi.eventListener(this.selfGreetInputLab.querySelector("input"), "blur", () => {
            // 失去焦点，编辑的招呼语保存到内存中；用于msgPage每次实时获取到最新的，即便不保存
            ScriptConfig.setSelfGreetMemory(DOMApi.getInputVal(this.selfGreetInputLab))
        })
        // 每次刷新页面；将保存的数据覆盖内存临时数据；否则编辑了自定义招呼语，未保存刷新页面；发的的是之前内存中编辑的临时数据
        ScriptConfig.setSelfGreetMemory(this.scriptConfig.getSelfGreet())

        let inputContainerDiv = DOMApi.createTag("div", "", "margin: 10px 0px;");
        inputContainerDiv.appendChild(this.cnInInputLab)
        inputContainerDiv.appendChild(this.cnExInputLab)
        inputContainerDiv.appendChild(this.jnInInputLab)
        inputContainerDiv.appendChild(this.jcExInputLab)
        inputContainerDiv.appendChild(this.srInInputLab)
        inputContainerDiv.appendChild(this.csrInInputLab)
        inputContainerDiv.appendChild(this.selfGreetInputLab)

        // 进度显示
        this.showTable = this.buildShowTable();

        // 操作面板结构：
        let operationPanel = DOMApi.createTag("div");
        // 说明文档
        // 链接关于
        // 操作按钮
        // 筛选输入框
        // iframe【详情页投递内部页】
        operationPanel.appendChild(this.buildDocDiv())
        operationPanel.appendChild(inputContainerDiv)
        // 发送自定义招呼语的iframe
        operationPanel.appendChild(this.buildMsgPageIframe())
        operationPanel.appendChild(this.showTable)
        // 词云图模态框 加到根节点
        document.body.appendChild(this.buildWordCloudModel())

        // 找到页面锚点并将操作面板添加入页面
        let timingCutPageTask = setInterval(() => {
            logger.info("等待页面加载，添加操作面板")
            // 页面锚点
            const jobSearchWrapper = document.querySelector(".job-search-wrapper")
            if (!jobSearchWrapper) {
                return;
            }
            const jobConditionWrapper = jobSearchWrapper.querySelector(".search-condition-wrapper")
            if (!jobConditionWrapper) {
                return
            }
            let topTitle = DOMApi.createTag("h2");
            this.topTitle = topTitle;
            topTitle.textContent = `Boos直聘投递助手（${this.scriptConfig.getVal(ScriptConfig.PUSH_COUNT, 0)}次） 记得 star⭐`;
            jobConditionWrapper.insertBefore(topTitle, jobConditionWrapper.firstElementChild)
            // 按钮/搜索换位
            const jobSearchBox = jobSearchWrapper.querySelector(".job-search-box")
            jobSearchBox.style.margin = "20px 0"
            jobSearchBox.style.width = "100%"
            const city = jobConditionWrapper.querySelector(".city-area-select")
            city.querySelector(".city-area-current").style.width = "85px"
            const condition = jobSearchWrapper.querySelectorAll(".condition-industry-select,.condition-position-select,.condition-filter-select,.clear-search-btn")
            const cityAreaDropdown = jobSearchWrapper.querySelector(".city-area-dropdown")
            cityAreaDropdown.insertBefore(jobSearchBox, cityAreaDropdown.firstElementChild)
            const filter = DOMApi.createTag("div", "", "overflow：hidden ")
            condition.forEach(item => {
                filter.appendChild(item)
            })
            filter.appendChild(DOMApi.createTag("div", "", "clear:both"))
            cityAreaDropdown.appendChild(filter)
            const bttt = [batchPushBtn, generateImgBtn, storeConfigBtn, this.activeSwitchBtn, this.bossOnlineSwitchBtn, this.sendSelfGreetSwitchBtn, this.headhunterSwitchBtn]
            bttt.forEach(item => {
                jobConditionWrapper.appendChild(item);
            })
            cityAreaDropdown.appendChild(operationPanel);
            clearInterval(timingCutPageTask);
            logger.info("初始化【操作面板】成功")
            // 页面美化
            this.pageBeautification()
        }, 1000);
    }

    /**
     * 页面美化
     */
    pageBeautification() {
        // 侧栏
        DOMApi.delElement(".job-side-wrapper")
        // 侧边悬浮框
        DOMApi.delElement(".side-bar-box")
        // 新职位发布时通知我
        DOMApi.delElement(".subscribe-weixin-wrapper", true)
        // 搜索栏登录框
        DOMApi.delElement(".go-login-btn")
        // 搜索栏去APP
        DOMApi.delElement(".job-search-scan", true)
        // 顶部面板
        // DOMApi.setElement(".job-search-wrapper",{width:"90%"})
        // DOMApi.setElement(".page-job-content",{width:"90%"})
        // DOMApi.setElement(".job-list-wrapper",{width:"100%"})
        GM_addStyle(`
        .job-search-wrapper,.page-job-content{width: 90% !important}
        .job-list-wrapper,.job-card-wrapper,.job-search-wrapper.fix-top{width: 100% !important}
        .job-card-wrapper .job-card-body{display: flex;justify-content: space-between;}
        .job-card-wrapper .job-card-left{width: 50% !important}
        .job-card-wrapper .start-chat-btn,.job-card-wrapper:hover .info-public{display: initial !important}
        .job-card-wrapper .job-card-footer{min-height: 48px;display: flex;justify-content: space-between}
        .job-card-wrapper .clearfix:after{content: none}
        .job-card-wrapper .job-card-footer .info-desc{width: auto !important}
        .job-card-wrapper .job-card-footer .tag-list{width: auto !important;margin-right:10px}
        .city-area-select.pick-up .city-area-dropdown{width: 80vw;min-width: 1030px;}
        .job-search-box .job-search-form{width: 100%;}
        .job-search-box .job-search-form .city-label{width: 10%;}
        .job-search-box .job-search-form .search-input-box{width: 82%;}
        .job-search-box .job-search-form .search-btn{width: 8%;}
        .job-search-wrapper.fix-top .job-search-box, .job-search-wrapper.fix-top .search-condition-wrapper{width: 90%;min-width:990px;}
        `)
        logger.info("初始化【页面美化】成功")
    }

    registerEvent() {
        TampermonkeyApi.GmAddValueChangeListener(ScriptConfig.PUSH_COUNT, this.publishCountChangeEventHandler.bind(this))
    }

    refreshShow(text) {
        this.showTable.innerHTML = "当前操作：" + text
    }

    refreshQuantity() {
        this.worldCloudAllBtn.innerHTML = `生成全部(${this.jobListHandler.cacheSize()}个)`
    }

    /*-------------------------------------------------构建复合DOM元素--------------------------------------------------*/

    buildDocDiv() {
        const docDiv = DOMApi.createTag("div", "", "margin: 10px 0px; width: 100%;")
        let txtDiv = DOMApi.createTag("div", "", "display: block;");
        const title = DOMApi.createTag("h3", "操作说明(点击关闭)", "margin: 10px 0px;cursor: pointer")

        docDiv.appendChild(title)
        docDiv.appendChild(txtDiv)
        this.docTextArr.forEach(doc => {
            const textTag = document.createElement("p");
            textTag.style.color = "#666";
            textTag.innerHTML = doc;
            txtDiv.appendChild(textTag)
        })

        this.aboutLink.forEach((linkMap) => {
            let about = DOMApi.createTag("p", "", "padding-top: 12px;");
            linkMap.forEach((item) => {
                const a = document.createElement("a");
                a.innerText = item[0];
                a.href = item[1];
                a.target = "_blank";
                a.style.margin = "0 20px 0 0";
                about.appendChild(a);
            });
            txtDiv.appendChild(about);
        });

        // 点击title，内部元素折叠
        DOMApi.eventListener(title, "click", () => {
            let divDisplay = txtDiv.style.display;
            if (divDisplay === 'block' || divDisplay === '') {
                txtDiv.style.display = 'none';
            } else {
                txtDiv.style.display = 'block';
            }
        })
        return docDiv;
    }

    buildMsgPageIframe() {
        let msgPageIframe = DOMApi.createTag("iframe", "", "height:1px;width: 1px;");
        msgPageIframe.src = 'https://www.zhipin.com/web/geek/chat';
        msgPageIframe.id = 'msgIframe';
        return msgPageIframe
    }


    buildShowTable() {
        return DOMApi.createTag('p', '', 'font-size: 20px;color: rgb(64, 158, 255);margin-left: 50px;');
    }

    buildWordCloudModel() {
        this.worldCloudModal = DOMApi.createTag("div", `
          <div class="dialog-layer"></div>
          <div class="dialog-container" style="width: 80%;height: 80%;">
            <div class="dialog-header">
              <h3>词云图</h3>
               <span class="close"><i class="icon-close"></i></span>
            </div>
            <div class="dialog-body" style="height: 98%;width: 100%;display: flex;flex-direction: column;">
               <div id="worldCloudCanvas" class="dialog-body" style="height: 100%;width: 100%;flex-grow: inherit;"></div>
            </div>
          </div>
        `, "display: none;")
        const model = this.worldCloudModal
        model.className = "dialog-wrap"
        model.querySelector(".close").onclick = function () {
            model.style.display = "none";
        }
        const body = model.querySelector(".dialog-body")
        const div = DOMApi.createTag("div")
        let btnCssText = "display: inline-block;border-radius: 4px;background: #e5f8f8;color: #00a6a7; text-decoration: none;margin: 0px 20px;padding: 6px 12px;cursor: pointer";
        // 当前状态
        let stateBtn = DOMApi.createTag("div", "状态: 工作标签", btnCssText);
        DOMApi.eventListener(stateBtn, "click", () => {
            if (this.worldCloudState) {
                stateBtn.innerHTML = "状态: 工作标签"
            } else {
                stateBtn.innerHTML = "状态: 工作内容"
            }
            this.worldCloudState = !this.worldCloudState
        })
        // 爬取当前页面生成词云
        let curBtn = DOMApi.createTag("div", "生成当前页", btnCssText);
        DOMApi.eventListener(curBtn, "click", () => {
            if (this.worldCloudState) {
                this.generateImgHandler()
            } else {
                this.generateImgHandlerJobLabel()
            }
        })
        // 根据已爬取的数据生成词云
        let allBtn = DOMApi.createTag("div", "生成全部(0个)", btnCssText);
        DOMApi.eventListener(allBtn, "click", () => {
            if (this.worldCloudState) {
                // this.generateImgHandlerAll()
                window.alert("卡顿严重,数据量大已禁用,请用标签模式")
            } else {
                this.generateImgHandlerJobLabelAll()
            }
        })
        this.worldCloudAllBtn = allBtn
        // 清空已爬取的数据
        let delBtn = DOMApi.createTag("div", "清空数据", btnCssText);
        DOMApi.eventListener(delBtn, "click", () => {
            this.jobListHandler.cacheClear()
            this.refreshQuantity()
        })
        div.appendChild(stateBtn)
        div.appendChild(curBtn)
        div.appendChild(allBtn)
        div.appendChild(delBtn)
        body.insertBefore(div, body.firstElementChild)
        return this.worldCloudModal
    }

    /*-------------------------------------------------操作面板事件处理--------------------------------------------------*/


    batchPushBtnHandler() {
        this.jobListHandler.batchPushHandler()

    }

    /**
     * 生成词云图
     * 使用的数据源为 job工作内容，进行分词
     */
    generateImgHandler() {
        let jobList = BossDOMApi.getJobList();
        let allJobContent = ""
        this.refreshShow("生成词云图【获取Job数据中】")
        Array.from(jobList).reduce((promiseChain, jobTag) => {
            return promiseChain
                .then(() => this.jobListHandler.reqJobDetail(jobTag))
                .then(jobCardJson => {
                    allJobContent += jobCardJson.postDescription + ""
                })
        }, Promise.resolve())
            .then(() => {
                this.refreshShow("生成词云图【构建数据中】")
                return JobWordCloud.participle(allJobContent)
            }).then(worldArr => {
            let weightWordArr = JobWordCloud.buildWord(worldArr);
            logger.info("根据权重排序的world结果：", JobWordCloud.getKeyWorldArr(weightWordArr));
            JobWordCloud.generateWorldCloudImage("worldCloudCanvas", weightWordArr)
            this.refreshShow("生成词云图【完成】")
        })
    }

    /**
     * 生成词云图
     * 使用的数据源为 job标签，并且不进行分词，直接计算权重
     */
    generateImgHandlerJobLabel() {
        let jobList = BossDOMApi.getJobList();
        let jobLabelArr = []
        this.refreshShow("生成词云图【获取Job数据中】")
        Array.from(jobList).reduce((promiseChain, jobTag) => {
            return promiseChain
                .then(() => this.jobListHandler.reqJobDetail(jobTag))
                .then(jobCardJson => {
                    jobLabelArr.push(...jobCardJson.jobLabels)
                })
        }, Promise.resolve())
            .then(() => {
                this.refreshShow("生成词云图【构建数据中】")
                let weightWordArr = JobWordCloud.buildWord(jobLabelArr);
                logger.info("根据权重排序的world结果：", JobWordCloud.getKeyWorldArr(weightWordArr));
                this.worldCloudModal.style.display = "flex"
                JobWordCloud.generateWorldCloudImage("worldCloudCanvas", weightWordArr)
                this.refreshShow("生成词云图【完成】")
            })
    }

    /**
     * 生成All词云图
     * 使用的数据源为 job工作内容，进行分词
     */
    generateImgHandlerAll() {
        let allJobContent = ""
        this.jobListHandler.cache.forEach((val) => {
            allJobContent += val.postDescription
        })
        Promise.resolve()
            .then(() => {
                this.refreshShow("生成词云图【构建数据中】")
                return JobWordCloud.participle(allJobContent)
            }).then(worldArr => {
            let weightWordArr = JobWordCloud.buildWord(worldArr);
            logger.info("根据权重排序的world结果：", JobWordCloud.getKeyWorldArr(weightWordArr));
            JobWordCloud.generateWorldCloudImage("worldCloudCanvas", weightWordArr)
            this.refreshShow("生成词云图【完成】")
        })
    }

    /**
     * 生成All词云图
     * 使用的数据源为 job标签，并且不进行分词，直接计算权重
     */
    generateImgHandlerJobLabelAll() {
        let jobLabelArr = []
        this.jobListHandler.cache.forEach((val) => {
            jobLabelArr.push(...val.jobLabels)
        })
        this.refreshShow("生成词云图【获取Job数据中】")
        Promise.resolve()
            .then(() => {
                this.refreshShow("生成词云图【构建数据中】")
                let weightWordArr = JobWordCloud.buildWord(jobLabelArr);
                logger.info("根据权重排序的world结果：", JobWordCloud.getKeyWorldArr(weightWordArr));
                this.worldCloudModal.style.display = "flex"
                JobWordCloud.generateWorldCloudImage("worldCloudCanvas", weightWordArr)
                this.refreshShow("生成词云图【完成】")
            })
    }


    readInputConfig() {
        this.scriptConfig.setCompanyNameInclude(DOMApi.getInputVal(this.cnInInputLab))
        this.scriptConfig.setCompanyNameExclude(DOMApi.getInputVal(this.cnExInputLab))
        this.scriptConfig.setJobNameInclude(DOMApi.getInputVal(this.jnInInputLab))
        this.scriptConfig.setJobContentExclude(DOMApi.getInputVal(this.jcExInputLab))
        this.scriptConfig.setSalaryRange(DOMApi.getInputVal(this.srInInputLab))
        this.scriptConfig.setCompanyScaleRange(DOMApi.getInputVal(this.csrInInputLab))
        this.scriptConfig.setSelfGreet(DOMApi.getInputVal(this.selfGreetInputLab))
    }

    storeConfigBtnHandler() {
        // 先修改配置对象内存中的值，然后更新到本地储存中
        this.readInputConfig()
        logger.info("config", this.scriptConfig)
        this.scriptConfig.storeConfig()
    }

    activeSwitchBtnHandler(isOpen) {
        this.bossActiveState = isOpen;
        if (this.bossActiveState) {
            this.activeSwitchBtn.innerText = "过滤不活跃Boss:已开启";
            this.activeSwitchBtn.style.backgroundColor = "rgb(215,254,195)";
            this.activeSwitchBtn.style.color = "rgb(2,180,6)";
        } else {
            this.activeSwitchBtn.innerText = "过滤不活跃Boss:已关闭";
            this.activeSwitchBtn.style.backgroundColor = "rgb(251,224,224)";
            this.activeSwitchBtn.style.color = "rgb(254,61,61)";
        }
        this.scriptConfig.setVal(ScriptConfig.ACTIVE_ENABLE, isOpen)
    }

    //检查是否在线
    bossOnlineSwitchBtnHandler(isOpen) {
        this.bossOnlineState = isOpen;
        if (this.bossOnlineState) {
            this.bossOnlineSwitchBtn.innerText = "过滤HR在线:已开启";
            this.bossOnlineSwitchBtn.style.backgroundColor = "rgb(215,254,195)";
            this.bossOnlineSwitchBtn.style.color = "rgb(2,180,6)";
        } else {
            this.bossOnlineSwitchBtn.innerText = "过滤HR在线:已关闭";
            this.bossOnlineSwitchBtn.style.backgroundColor = "rgb(251,224,224)";
            this.bossOnlineSwitchBtn.style.color = "rgb(254,61,61)";
        }
        this.scriptConfig.setVal(ScriptConfig.BOSS_ONLINE_ENABLE, isOpen)
    }

    sendSelfGreetSwitchBtnHandler(isOpen) {
        this.sendSelfGreet = isOpen;
        if (isOpen) {
            this.sendSelfGreetSwitchBtn.innerText = "发送自定义招呼语:已开启";
            this.sendSelfGreetSwitchBtn.style.backgroundColor = "rgb(215,254,195)";
            this.sendSelfGreetSwitchBtn.style.color = "rgb(2,180,6)";
        } else {
            this.sendSelfGreetSwitchBtn.innerText = "发送自定义招呼语:已关闭";
            this.sendSelfGreetSwitchBtn.style.backgroundColor = "rgb(251,224,224)";
            this.sendSelfGreetSwitchBtn.style.color = "rgb(254,61,61)";
        }
        this.scriptConfig.setVal(ScriptConfig.SEND_SELF_GREET_ENABLE, isOpen)
    }

    sendHeadhunterSwitchBtnHandler(isOpen) {
        this.headhunterState = isOpen;
        if (isOpen) {
            this.headhunterSwitchBtn.innerText = "过滤猎头岗位:已开启";
            this.headhunterSwitchBtn.style.backgroundColor = "rgb(215,254,195)";
            this.headhunterSwitchBtn.style.color = "rgb(2,180,6)";
        } else {
            this.headhunterSwitchBtn.innerText = "过滤猎头岗位:已关闭";
            this.headhunterSwitchBtn.style.backgroundColor = "rgb(251,224,224)";
            this.headhunterSwitchBtn.style.color = "rgb(254,61,61)";
        }
        this.scriptConfig.setVal(ScriptConfig.SEND_HEADHUNTER_ENABLE, isOpen)
    }

    publishCountChangeEventHandler(key, oldValue, newValue, isOtherScriptChange) {
        this.topTitle.textContent = `Boos直聘投递助手（${newValue}次） 记得 star⭐`;
        logger.info("投递次数变更事件", {key, oldValue, newValue, isOtherScriptChange})
    }

    /*-------------------------------------------------other method--------------------------------------------------*/

    changeBatchPublishBtn(start) {
        if (start) {
            this.batchPushBtn.innerHTML = "停止投递"
            this.batchPushBtn.style.backgroundColor = "rgb(251,224,224)";
            this.batchPushBtn.style.color = "rgb(254,61,61)";
        } else {
            this.batchPushBtn.innerHTML = "批量投递"
            this.batchPushBtn.style.backgroundColor = "rgb(215,254,195)";
            this.batchPushBtn.style.color = "rgb(2,180,6)";
        }
    }
}

class ScriptConfig extends TampermonkeyApi {

    static LOCAL_CONFIG = "config";
    static PUSH_COUNT = "pushCount:" + ScriptConfig.getCurDay();
    static ACTIVE_ENABLE = "activeEnable";
    static PUSH_LIMIT = "push_limit" + ScriptConfig.getCurDay();
    // 投递锁是否被占用，可重入；value表示当前正在投递的job
    static PUSH_LOCK = "push_lock";

    static PUSH_MESSAGE = "push_message";
    static SEND_SELF_GREET_ENABLE = "sendSelfGreetEnable";
    static SEND_HEADHUNTER_ENABLE = "sendHeadhunterEnable";

    //HR在线检测
    static BOSS_ONLINE_ENABLE = "bossOnlineEnable";

    // 公司名包含输入框lab
    static cnInKey = "companyNameInclude"
    // 公司名排除输入框lab
    static cnExKey = "companyNameExclude"
    // job名称包含输入框lab
    static jnInKey = "jobNameInclude"
    // job内容排除输入框lab
    static jcExKey = "jobContentExclude"
    // 薪资范围输入框lab
    static srInKey = "salaryRange"
    // 公司规模范围输入框lab
    static csrInKey = "companyScaleRange"
    // 自定义招呼语输入框
    static sgInKey = "sendSelfGreet"
    static SEND_SELF_GREET_MEMORY = "sendSelfGreetMemory"


    constructor() {
        super();
        this.configObj = {}

        this.loaderConfig()
    }

    static getCurDay() {
        // 创建 Date 对象获取当前时间
        const currentDate = new Date();

        // 获取年、月、日、小时、分钟和秒
        const year = currentDate.getFullYear();
        const month = String(currentDate.getMonth() + 1).padStart(2, '0');
        const day = String(currentDate.getDate()).padStart(2, '0');

        // 格式化时间字符串
        return `${year}-${month}-${day}`;
    }

    static pushCountIncr() {
        let number = TampermonkeyApi.GmGetValue(ScriptConfig.PUSH_COUNT, 0);
        TampermonkeyApi.GmSetValue(ScriptConfig.PUSH_COUNT, ++number)
    }

    getVal(key, defVal) {
        return TampermonkeyApi.GmGetValue(key, defVal)
    }

    setVal(key, val) {
        TampermonkeyApi.GmSetValue(key, val)
    }

    getArrConfig(key, isArr) {
        let arr = this.configObj[key];
        if (isArr) {
            return arr;
        }
        if (!arr) {
            return "";
        }
        return arr.join(",");
    }

    getStrConfig(key) {
        let str = this.configObj[key];
        if (!str) {
            return "";
        }
        return str;
    }

    getCompanyNameInclude(isArr) {
        return this.getArrConfig(ScriptConfig.cnInKey, isArr);
    }


    getCompanyNameExclude(isArr) {
        return this.getArrConfig(ScriptConfig.cnExKey, isArr);
    }

    getJobContentExclude(isArr) {
        return this.getArrConfig(ScriptConfig.jcExKey, isArr);
    }

    getJobNameInclude(isArr) {
        return this.getArrConfig(ScriptConfig.jnInKey, isArr);
    }


    getSalaryRange() {
        return this.getStrConfig(ScriptConfig.srInKey);
    }

    getCompanyScaleRange() {
        return this.getStrConfig(ScriptConfig.csrInKey);
    }

    getSelfGreet() {
        return this.getStrConfig(ScriptConfig.sgInKey);
    }


    setCompanyNameInclude(val) {
        return this.configObj[ScriptConfig.cnInKey] = val.split(",");
    }

    setCompanyNameExclude(val) {
        this.configObj[ScriptConfig.cnExKey] = val.split(",");
    }

    setJobNameInclude(val) {
        this.configObj[ScriptConfig.jnInKey] = val.split(",");
    }

    setJobContentExclude(val) {
        this.configObj[ScriptConfig.jcExKey] = val.split(",");
    }


    setSalaryRange(val) {
        this.configObj[ScriptConfig.srInKey] = val;
    }

    setCompanyScaleRange(val) {
        this.configObj[ScriptConfig.csrInKey] = val;
    }

    setSelfGreet(val) {
        this.configObj[ScriptConfig.sgInKey] = val;
    }

    static setSelfGreetMemory(val) {
        TampermonkeyApi.GmSetValue(ScriptConfig.SEND_SELF_GREET_MEMORY, val)
    }

    getSelfGreetMemory() {
        let value = TampermonkeyApi.GmGetValue(ScriptConfig.SEND_SELF_GREET_MEMORY);
        if (value) {
            return value;
        }

        return this.getSelfGreet();
    }

    /**
     * 存储配置到本地存储中
     */
    storeConfig() {
        let configStr = JSON.stringify(this.configObj);
        TampermonkeyApi.GmSetValue(ScriptConfig.LOCAL_CONFIG, configStr);
        logger.info("存储配置到本地储存", configStr)
    }

    /**
     * 从本地存储中加载配置
     */
    loaderConfig() {
        let localConfig = TampermonkeyApi.GmGetValue(ScriptConfig.LOCAL_CONFIG, "");
        if (!localConfig) {
            logger.warn("未加载到本地配置")
            return;
        }

        this.configObj = JSON.parse(localConfig);
        logger.info("成功加载本地配置", this.configObj)
    }


}

class BossDOMApi {


    static getJobList() {
        return document.querySelectorAll(".job-card-wrapper");
    }

    static getJobTitle(jobTag) {
        let innerText = jobTag.querySelector(".job-title").innerText;
        return innerText.replace("\n", " ");
    }

    //是猎头发布的职位吗？
    static isHeadhunter(jobTag) {
        let jobTagIcon = jobTag.querySelector("img.job-tag-icon");
        return !!jobTagIcon;
    }

    static getCompanyName(jobTag) {
        return jobTag.querySelector(".company-name").innerText;
    }

    static getJobName(jobTag) {
        return jobTag.querySelector(".job-name").innerText;
    }

    static getSalaryRange(jobTag) {
        let text = jobTag.querySelector(".salary").innerText;
        if (text.includes(".")) {
            // 1-2K·13薪
            return text.split("·")[0];
        }
        return text;
    }

    static getCompanyScaleRange(jobTag) {
        return jobTag.querySelector(".company-tag-list").lastElementChild.innerHTML;
    }

    /**
     * 获取当前job标签的招聘人名称以及他的职位
     * @param jobTag
     */
    static getBossNameAndPosition(jobTag) {
        let nameAndPositionTextArr = jobTag.querySelector(".info-public").innerHTML.split("<em>");
        nameAndPositionTextArr[0] = nameAndPositionTextArr[0].trim();
        nameAndPositionTextArr[1] = nameAndPositionTextArr[1].replace("</em>", "").trim();
        return nameAndPositionTextArr;
    }

    /**
     * 是否为未沟通
     * @param jobTag
     */
    static isNotCommunication(jobTag) {
        const jobStatusStr = jobTag.querySelector(".start-chat-btn").innerText;
        return jobStatusStr.includes("立即沟通");
    }

    static getJobDetailUrlParams(jobTag) {
        return jobTag.querySelector(".job-card-left").href.split("?")[1]
    }

    static getDetailSrc(jobTag) {
        return jobTag.querySelector(".job-card-left").href;
    }

    static getUniqueKey(jobTag) {
        const title = this.getJobTitle(jobTag)
        const company = this.getCompanyName(jobTag)
        return `${title}--${company}`
    }

    static nextPage() {
        let nextPageBtn = document.querySelector(".ui-icon-arrow-right");

        if (nextPageBtn.parentElement.className === "disabled") {
            // 没有下一页
            return;

        }
        nextPageBtn.click();
        return true;
    }
}


class JobListPageHandler {

    constructor() {
        this.operationPanel = new OperationPanel(this);
        this.scriptConfig = this.operationPanel.scriptConfig
        this.operationPanel.init()
        this.publishState = false
        this.nextPage = false
        this.mock = false
        this.cache = new Map()
        this.selfDefCount = -1
    }

    /**
     * 点击批量投递事件处理
     */
    batchPushHandler() {
        this.changeBatchPublishState(!this.publishState);
        if (!this.publishState) {
            return;
        }
        // 每次投递前清空投递锁，未被占用
        this.scriptConfig.setVal(ScriptConfig.PUSH_LIMIT, false)
        TampermonkeyApi.GmSetValue(ScriptConfig.PUSH_LOCK, "")
        // 每次读取操作面板中用户实时输入的值
        this.operationPanel.readInputConfig()

        this.loopPublish()
    }

    loopPublish() {
        // 过滤当前页满足条件的job并投递
        this.filterCurPageAndPush()

        // 等待处理完当前页的jobList在投递下一页
        let nextPageTask = setInterval(() => {
            if (!this.nextPage) {
                logger.info("正在等待当前页投递完毕...")
                return;
            }
            clearInterval(nextPageTask)

            if (!this.publishState) {
                logger.info("投递结束")
                TampermonkeyApi.GmNotification("投递结束")
                this.operationPanel.refreshShow("投递停止")
                this.changeBatchPublishState(false);
                return;
            }
            if (!BossDOMApi.nextPage()) {
                logger.info("投递结束，没有下一页")
                TampermonkeyApi.GmNotification("投递结束，没有下一页")
                this.operationPanel.refreshShow("投递结束，没有下一页")
                this.changeBatchPublishState(false);
                return;
            }
            this.operationPanel.refreshShow("开始等待 10 秒钟,进行下一页")
            // 点击下一页，需要等待页面元素变化，否则将重复拿到当前页的jobList
            setTimeout(() => {
                this.loopPublish()
            }, 10000)
        }, 3000);
    }

    changeBatchPublishState(publishState) {
        this.publishState = publishState;
        this.operationPanel.changeBatchPublishBtn(publishState)
    }

    filterCurPageAndPush() {
        this.nextPage = false;
        let notMatchCount = 0;
        let publishResultCount = {
            successCount: 0,
            failCount: 0,
        }
        let jobList = BossDOMApi.getJobList();
        logger.info("jobList", jobList)
        let process = Array.from(jobList).reduce((promiseChain, jobTag) => {
            let jobTitle = BossDOMApi.getJobTitle(jobTag);
            return promiseChain
                .then(() => this.matchJobPromise(jobTag))
                .then(() => this.reqJobDetail(jobTag))
                .then(jobCardJson => this.jobDetailFilter(jobTag, jobCardJson))
                .then(() => this.sendPublishReq(jobTag))
                .then(publishResult => this.handlerPublishResult(jobTag, publishResult, publishResultCount))
                .catch(error => {
                    // 在catch中return是结束当前元素，不会结束整个promiseChain；
                    // 需要结束整个promiseChain，在catch throw exp,但还会继续执行下一个元素catch中的逻辑
                    switch (true) {
                        case error instanceof JobNotMatchExp:
                            this.operationPanel.refreshShow(jobTitle + " 不满足投递条件")
                            ++notMatchCount;
                            break;

                        case error instanceof FetchJobDetailFailExp:
                            logger.error("job详情页数据获取失败：" + error);
                            break;

                        case error instanceof SendPublishExp:
                            logger.error("投递失败;" + jobTitle + " 原因：" + error.message);
                            this.operationPanel.refreshShow(jobTitle + " 投递失败")
                            publishResultCount.failCount++
                            break;

                        case error instanceof PublishLimitExp:
                            TampermonkeyApi.GmSetValue(ScriptConfig.PUSH_LIMIT, true);
                            this.operationPanel.refreshShow("停止投递 " + error.message)
                            logger.error("投递停止; 原因：" + error.message);
                            throw new PublishStopExp(error.message)

                        case error instanceof PublishStopExp:
                            this.changeBatchPublishState(false)
                            // 结束整个投递链路
                            throw error;
                        default:
                            logger.info(BossDOMApi.getDetailSrc(jobTag) + "-->未捕获投递异常:", error);
                    }
                })
        }, Promise.resolve()).catch(error => {
            // 这里只是让报错不显示，不需要处理异常

        });


        // 当前页jobList中所有job处理完毕执行
        process.finally(() => {
            logger.info("当前页投递完毕---------------------------------------------------")
            logger.info("不满足条件的job数量：" + notMatchCount)
            logger.info("投递Job成功数量：" + publishResultCount.successCount)
            logger.info("投递Job失败数量：" + publishResultCount.failCount)
            logger.info("当前页投递完毕---------------------------------------------------")
            this.nextPage = true;
        })
    }

    cacheClear() {
        this.cache.clear()
    }

    cacheSize() {
        return this.cache.size
    }

    reqJobDetail(jobTag, retries = 3) {
        return new Promise((resolve, reject) => {
            if (retries === 0) {
                return reject(new FetchJobDetailFailExp());
            }
            // todo 如果在投递当前页中，点击停止投递，那么当前页重新投递的话，会将已经投递的再重新投递一遍
            //  原因是没有重新获取数据；沟通状态还是立即沟通，实际已经投递过一遍，已经为继续沟通
            //  暂时不影响逻辑，重复投递，boss自己会过滤，不会重复发送消息；发送自定义招呼语也没问题；油猴会过滤【oldVal===newVal】的数据，也就不会重复发送自定义招呼语
            const key = BossDOMApi.getUniqueKey(jobTag)
            if (this.cache.has(key)) {
                return resolve(this.cache.get(key))
            }
            let params = BossDOMApi.getJobDetailUrlParams(jobTag);
            axios.get("https://www.zhipin.com/wapi/zpgeek/job/card.json?" + params, {timeout: 5000})
                .then(resp => {
                    this.cache.set(key, resp.data.zpData.jobCard)
                    return resolve(resp.data.zpData.jobCard);
                }).catch(error => {
                logger.info("获取详情页异常正在重试:", error)
                return this.reqJobDetail(jobTag, retries - 1)
            })
        })
    }

    jobDetailFilter(jobTag, jobCardJson) {
        let jobTitle = BossDOMApi.getJobTitle(jobTag);

        return new Promise((resolve, reject) => {

            //检查是否在线
            let bossOnlineCheck = TampermonkeyApi.GmGetValue(ScriptConfig.BOSS_ONLINE_ENABLE, true);
            logger.info("当前职位【" + jobTitle + "】HR在线状态：" + jobCardJson.online)
            if (bossOnlineCheck && !jobCardJson.online) {
                logger.info("当前job被过滤：【" + jobTitle + "】 HR原因：不在线")
                return reject(new JobNotMatchExp())
            }

            // 工作详情活跃度检查
            let activeCheck = TampermonkeyApi.GmGetValue(ScriptConfig.ACTIVE_ENABLE, true);
            let activeTimeDesc = jobCardJson.activeTimeDesc;
            if (activeCheck && !Tools.bossIsActive(activeTimeDesc)) {
                logger.info("当前boss活跃度：" + activeTimeDesc)
                logger.info("当前job被过滤：【" + jobTitle + "】 原因：不满足活跃度检查")
                return reject(new JobNotMatchExp())
            }

            // 猎头工作岗位检查
            let headhunterCheck = TampermonkeyApi.GmGetValue(ScriptConfig.SEND_HEADHUNTER_ENABLE, true);
            if (headhunterCheck && BossDOMApi.isHeadhunter(jobTag)) {
                logger.info("当前工作为猎头发布：" + jobTitle);
                logger.info("当前job被过滤：【" + jobTitle + "】 原因：为猎头发布的工作");
                return reject(new JobNotMatchExp());
            }

            // 工作内容检查
            let jobContentExclude = this.scriptConfig.getJobContentExclude(true);
            const jobContentMismatch = Tools.semanticMatch(jobContentExclude, jobCardJson.postDescription)
            if (jobContentMismatch) {
                logger.info("当前job工作内容：" + jobCardJson.postDescription)
                logger.info(`当前job被过滤：【${jobTitle}】 原因：不满足工作内容(${jobContentMismatch})`)
                return reject(new JobNotMatchExp())
            }


            setTimeout(() => {
                // 获取不同的延时，避免后面投递时一起导致频繁
                return resolve();
            }, Tools.getRandomNumber(100, 200))
        })
    }

    handlerPublishResult(jobTag, result, publishResultCount) {
        return new Promise((resolve, reject) => {
            if (result.message === 'Success' && result.code === 0) {
                // 增加投递数量，触发投递监听，更新页面投递计数
                ScriptConfig.pushCountIncr()
                publishResultCount.successCount++
                logger.info("投递成功：" + BossDOMApi.getJobTitle(jobTag))

                // 改变消息key，通知msg页面处理当前job发送自定义招呼语句
                TampermonkeyApi.GmSetValue(ScriptConfig.PUSH_MESSAGE, JobMessagePageHandler.buildMsgKey(jobTag))

                // 每页投递次数【默认不会走】
                if (this.selfDefCount !== -1 && publishResultCount.successCount >= this.selfDefCount) {
                    return reject(new PublishStopExp("自定义投递限制：" + this.selfDefCount))
                }
                return resolve()
            }

            if (result.message.includes("今日沟通人数已达上限")) {
                return reject(new PublishLimitExp(result.message))
            }

            return reject(new SendPublishExp(result.message))
        })
    }

    sendPublishReq(jobTag, errorMsg, retries = 3) {
        let jobTitle = BossDOMApi.getJobTitle(jobTag);
        if (retries === 3) {
            logger.info("正在投递：" + jobTitle)
        }
        return new Promise((resolve, reject) => {
            if (retries === 0) {
                return reject(new SendPublishExp(errorMsg));
            }
            if (!this.publishState) {
                return reject(new PublishStopExp("停止投递"))
            }

            // 检查投递限制
            let pushLimit = TampermonkeyApi.GmGetValue(ScriptConfig.PUSH_LIMIT, false);
            if (pushLimit) {
                this.changeBatchPublishState(false)
                return reject(new PublishLimitExp("boss投递限制每天100次"))
            }

            if (this.mock) {
                let result = {
                    message: 'Success',
                    code: 0
                }
                return resolve(result)
            }

            let src = BossDOMApi.getDetailSrc(jobTag);
            let paramObj = Tools.parseURL(src);
            let publishUrl = "https://www.zhipin.com/wapi/zpgeek/friend/add.json"
            let url = Tools.queryString(publishUrl, paramObj);

            let pushLockTask = setInterval(() => {
                if (!this.publishState) {
                    clearInterval(pushLockTask)
                    return reject(new PublishStopExp())
                }
                let lock = TampermonkeyApi.GmGetValue(ScriptConfig.PUSH_LOCK, "");
                if (lock && lock !== jobTitle) {
                    return logger.info("投递锁被其他job占用：" + lock)
                }
                // 停止锁检查并占用投递锁
                clearInterval(pushLockTask)
                TampermonkeyApi.GmSetValue(ScriptConfig.PUSH_LOCK, jobTitle)
                logger.info("锁定投递锁：" + jobTitle)

                this.operationPanel.refreshShow("正在投递-->" + jobTitle)
                // 投递请求
                axios.post(url, null, {headers: {"Zp_token": Tools.getCookieValue("bst")}})
                    .then(resp => {
                        if (resp.data.code === 1 && resp.data?.zpData?.bizData?.chatRemindDialog?.content) {
                            // 某些条件不满足，boss限制投递，无需重试，在结果处理器中处理
                            return resolve({
                                code: 1,
                                message: resp.data?.zpData?.bizData?.chatRemindDialog?.content
                            })
                        }

                        if (resp.data.code !== 0) {
                            throw new SendPublishExp(resp.data.message)
                        }
                        return resolve(resp.data);
                    }).catch(error => {
                    logger.info("投递异常正在重试:" + jobTitle, error)
                    return resolve(this.sendPublishReq(jobTag, error.message, retries - 1))
                }).finally(() => {
                    // 释放投递锁
                    logger.info("释放投递锁：" + jobTitle)
                    TampermonkeyApi.GmSetValue(ScriptConfig.PUSH_LOCK, "")
                })
            }, 800);
        })
    }


    matchJobPromise(jobTag) {
        return new Promise(((resolve, reject) => {
            if (!this.matchJob(jobTag)) {
                return reject(new JobNotMatchExp())
            }
            return resolve(jobTag)
        }))
    }

    matchJob(jobTag) {
        let jobTitle = BossDOMApi.getJobTitle(jobTag);
        let pageCompanyName = BossDOMApi.getCompanyName(jobTag);

        // 不满足配置公司名
        if (!Tools.fuzzyMatch(this.scriptConfig.getCompanyNameInclude(true),
            pageCompanyName, true)) {
            logger.info("当前公司名：" + pageCompanyName)
            logger.info("当前job被过滤：【" + jobTitle + "】 原因：不满足配置公司名")
            return false;
        }

        // 满足排除公司名
        if (Tools.fuzzyMatch(this.scriptConfig.getCompanyNameExclude(true),
            pageCompanyName, false)) {
            logger.info("当前公司名：" + pageCompanyName)
            logger.info("当前job被过滤：【" + jobTitle + "】 原因：满足排除公司名")
            return false;
        }

        // 不满足配置工作名
        let pageJobName = BossDOMApi.getJobName(jobTag);
        if (!Tools.fuzzyMatch(this.scriptConfig.getJobNameInclude(true),
            pageJobName, true)) {
            logger.info("当前工作名：" + pageJobName)
            logger.info("当前job被过滤：【" + jobTitle + "】 原因：不满足配置工作名")
            return false;
        }

        // 不满足新增范围
        let pageSalaryRange = BossDOMApi.getSalaryRange(jobTag);
        let salaryRange = this.scriptConfig.getSalaryRange();
        if (!Tools.rangeMatch(salaryRange, pageSalaryRange)) {
            logger.info("当前薪资范围：" + pageSalaryRange)
            logger.info("当前job被过滤：【" + jobTitle + "】 原因：不满足薪资范围")
            return false;
        }


        let pageCompanyScaleRange = this.scriptConfig.getCompanyScaleRange();
        if (!Tools.rangeMatch(pageCompanyScaleRange, BossDOMApi.getCompanyScaleRange(jobTag))) {
            logger.info("当前公司规模范围：" + pageCompanyScaleRange)
            logger.info("当前job被过滤：【" + jobTitle + "】 原因：不满足公司规模范围")
            return false;
        }

        if (!BossDOMApi.isNotCommunication(jobTag)) {
            logger.info("当前job被过滤：【" + jobTitle + "】 原因：已经沟通过")
            return false;
        }

        return true;
    }
}

class JobMessagePageHandler {

    constructor() {
        this.scriptConfig = new ScriptConfig();
        this.init()
    }

    init() {
        this.registerEvent();
    }

    registerEvent() {
        TampermonkeyApi.GmAddValueChangeListener(ScriptConfig.PUSH_MESSAGE, this.pushAlterMsgHandler.bind(this))
        logger.info("注册投递推送消费者成功")
    }

    /**
     * 投递后发送自定义打招呼语句【发送自定义消息】
     */
    pushAlterMsgHandler(key, oldValue, newValue, isOtherScriptChange) {
        logger.info("投递后推送自定义招呼语消费者", {key, oldValue, newValue, isOtherScriptChange})
        if (!isOtherScriptChange) {
            return;
        }
        if (oldValue === newValue) {
            return;
        }

        // 是否打开配置
        if (!TampermonkeyApi.GmGetValue(ScriptConfig.SEND_SELF_GREET_ENABLE, false)) {
            return;
        }

        let selfGreetMsg = this.getSelfGreet();
        if (!selfGreetMsg) {
            logger.info("自定义招呼语为空结束")
            return;
        }

        let count = 0;
        let process = Promise.resolve()
        let sendMsgTask = setInterval(() => {
            process.then(() => {
                if (++count >= 5) {
                    logger.info("发送自定义打招呼语句超时结束")
                    clearInterval(sendMsgTask);
                    return;
                }
                return new Promise(async (resolve, reject) => {
                    let msgTag = await JobMessagePageHandler.selectMessage(newValue);
                    if (!msgTag) {
                        return reject();
                    }
                    // 点击当前待处理的消息框
                    msgTag.click();
                    logger.info("选中消息", msgTag)
                    return resolve();
                })
            }).then(() => {
                return new Promise((resolve, reject) => {
                    if (!JobMessagePageHandler.ableInput()) {
                        return reject();
                    }
                    return resolve();
                })
            }).then(() => {
                return new Promise((resolve => {
                    JobMessagePageHandler.inputMsg(selfGreetMsg)
                    return resolve();
                }))
            }).then(() => {
                return new Promise(((resolve, reject) => {
                    if (!JobMessagePageHandler.sendAble()) {
                        return reject();
                    }
                    return resolve();
                }))
            }).then(() => {
                return new Promise((resolve => {
                    JobMessagePageHandler.sendMsg()
                    logger.info("推送自定义招呼语成功：" + newValue)
                    clearInterval(sendMsgTask)
                    return resolve()
                }))
            }).catch(() => {
                // 不报错
            })
        }, 500);
    }

    getSelfGreet() {
        return this.scriptConfig.getSelfGreetMemory();
    }

    static buildMsgKey(jobTag) {
        let companyName = BossDOMApi.getCompanyName(jobTag);
        let bossNameAndPosition = BossDOMApi.getBossNameAndPosition(jobTag);

        let bossName = bossNameAndPosition[0];
        let bossPositionName = bossNameAndPosition[1];
        return bossName + companyName + bossPositionName;
    }

    static ableInput() {
        return document.querySelector(".chat-input") && document.querySelector(".chat-im.chat-editor");
    }

    static inputMsg(msg) {
        // <br> \n 都可以换行
        return document.querySelector(".chat-input").innerHTML = msg.replaceAll("\\n", "\n");
    }

    static sendAble() {
        let btn = document.querySelector(".btn-v2.btn-sure-v2.btn-send");
        // 删除按钮标签类名；按钮可点击
        btn.classList.remove("disabled");
        return btn;
    }

    static sendMsg() {
        // 当前标签绑定的vue组件对象，
        let chatFrameVueComponent = document.querySelector(".chat-im.chat-editor").__vue__;
        // 更新开启提交；否则提交拦截
        chatFrameVueComponent.enableSubmit = true;
        // 赋值发送websocket的to.uid;手动触发导致uid无值，从friendId获取
        chatFrameVueComponent.bossInfo$.uid = chatFrameVueComponent.bossInfo$.friendId;
        let element = document.querySelector(".btn-v2.btn-sure-v2.btn-send");
        element.click();
    }

    static async getMessageListTag() {
        return new Promise((resolve) => {
            document.querySelector("li.selected").click();
            //等待bom渲染后获取
            setTimeout(() => {
                const lis = document.querySelector(".user-list").querySelector("div").querySelectorAll("li");
                resolve(lis);
            }, 100);
        });
    }

    static async selectMessage(messageKey) {
        let messageListTag = await JobMessagePageHandler.getMessageListTag();
        for (let i = 0; i < messageListTag.length; i++) {
            // '09月02日\n刘女士赛德勤人事行政专员\n您好，打扰了，我想和您聊聊这个职位。'
            // 日期\n【boss名+公司名+职位名】\n 问候语
            let msgTitle = messageListTag[i].innerText;
            if (msgTitle.split("\n")[1] === messageKey) {
                return messageListTag[i].querySelector("div");
            }
        }

        logger.info("本次循环消息key未检索到消息框: " + messageKey)
    }
}


class JobWordCloud {

    // 不应该使用分词，而应该是分句，结合上下文，自然语言处理
    static filterableWorldArr = ['', ' ', ',', '?', '+', '\n', '\r', "/", '有', '的', '等', '及', '了', '和', '公司', '熟悉', '服务', '并', '同', '如', '于', '或', '到',
        '开发', '技术', '我们', '提供', '武汉', '经验', '为', '在', '团队', '员工', '工作', '能力', '-', '1', '2', '3', '4', '5', '6', '7', '8', '', '年', '与', '平台', '研发', '行业',
        "实现", "负责", "代码", "精通", "图谱", "需求", "分析", "良好", "知识", "相关", "编码", "参与", "产品", "扎实", "具备", "较", "强", "沟通", "者", "优先", "具有", "精神", "编写", "功能", "完成", "详细", "岗位职责",
        "包括", "解决", "应用", "性能", "调", "优", "本科", "以上学历", "基础", "责任心", "高", "构建", "合作", "能", "学习", "以上", "熟练", "问题", "优质", "运行", "工具", "方案", "根据", "业务", "类", "文档", "分配",
        "其他", "亿", "级", "关系", "算法", "系统", "上线", "考虑", "工程师", "华为", "自动", "驾驶", "网络", "后", "端", "云", "高质量", "承担", "重点", "难点", "攻坚", "主导", "选型", "任务", "分解", "工作量", "评估",
        "创造性", "过程", "中", "提升", "核心", "竞争力", "可靠性", "要求", "计算机专业", "基本功", "ee", "主流", "微", "框架", "其", "原理", "推进", "优秀", "团队精神", "热爱", "可用", "大型", "网站", "表达", "理解能力",
        "同事", "分享", "愿意", "接受", "挑战", "拥有", "将", "压力", "转变", "动力", "乐观", "心态", "思路清晰", "严谨", "地", "习惯", "运用", "线", "上", "独立", "处理", "熟练掌握", "至少", "一种", "常见", "脚本", "环境",
        "搭建", "开发工具", "人员", "讨论", "制定", "用", "相应", "保证", "质量", "说明", "领导", "包含", "节点", "存储", "检索", "api", "基于", "数据", "落地", "个性化", "场景", "支撑", "概要", "按照", "规范", "所", "模块",
        "评审", "编译", "调试", "单元测试", "发布", "集成", "支持", "功能测试", "测试", "结果", "优化", "持续", "改进", "配合", "交付", "出现", "任职", "资格", "编程", "型", "使用", "认真负责", "高度", "责任感", "快速", "创新", "金融",

        "设计", "项目", "对", "常用", "掌握", "专业", "进行", "了解", "岗位", "能够", "中间件", "以及", "开源", "理解", ")", "软件", "计算机", "架构", "一定", "缓存", "可", "解决问题", "计算机相关", "发展", "时间", "奖金", "培训", "部署",
        "互联网", "享受", "善于", "需要", "游戏", " ", "维护", "统招", "语言", "消息", "机制", "逻辑思维", "一", "意识", "新", "攻关", "升级", "管理", "重构", "【", "职位", "】", "成员", "好", "接口", "语句", "后台", "通用", "不", "描述",
        "福利", "险", "机会", "会", "人", "完善", "技术难题", "技能", "应用服务器", "配置", "协助", "或者", "组织", "现有", "迭代", "流程", "项目管理", "从", "深入", "复杂", "专业本科", "协议", "不断", "项目经理", "协作", "五", "金", "待遇",
        "年终奖", "各类", "节日", "带薪", "你", "智慧", "前沿技术", "常用命令", "方案设计", "基本", "积极", "产品开发", "用户", "确保", "带领", "软件系统", "撰写", "软件工程", "职责", "抗压", "积极主动", "双休", "法定", "节假日", "假", "客户",
        "日常", "协同", "是", "修改", "要", "软件开发", "丰富", "乐于", "识别", "风险", "合理", "服务器", "指导", "规划", "提高", "稳定性", "扩展性", "功底", "钻研", "c", "高可用性", "计算机软件", "高效", "前端", "内部", "一起", "程序", "程序开发",
        "计划", "按时", "数理", "及其", "集合", "正式", "劳动合同", "薪资", "丰厚", "奖励", "补贴", "免费", "体检", "每年", "调薪", "活动", "职业", "素养", "晋升", "港", "氛围", "您", "存在", "关注", "停车", "参加", "系统分析", "发现", "稳定", "自主",
        "实际", "开发技术", "(", "一些", "综合", "条件", "学历", "薪酬", "维", "保", "全日制", "专科", "体系结构", "协调", "出差", "自测", "周一", "至", "周五", "周末", "公积金", "准备", "内容", "部门", "满足", "兴趣", "方式", "操作", "超过", "结合",
        "同时", "对接", "及时", "研究", "统一", "管控", "福利待遇", "政策", "办理", "凡是", "均", "丧假", "对于", "核心技术", "安全", "服务端", "游", "电商", "零售", "下", "扩展", "负载", "信息化", "命令", "供应链", "商业", "抽象", "模型", "领域", "瓶颈",
        "充分", "编程语言", "自我", "但", "限于", "应用软件", "适合", "各种", "大", "前后", "复用", "执行", "流行", "app", "小", "二", "多种", "转正", "空间", "盒", "马", "长期", "成长", "间", "通讯", "全过程", "提交", "目标", "电气工程", "阅读", "严密",
        "电力系统", "电力", "大小", "周", "心动", "入", "职", "即", "缴纳", "签署", "绩效奖金", "评优", "专利", "论文", "职称", "加班", "带薪休假", "专项", "健康", "每周", "运动", "休闲", "不定期", "小型", "团建", "旅游", "岗前", "牛", "带队", "答疑", "解惑",
        "晋级", "晋升为", "管理层", "跨部门", "转岗", "地点", "武汉市", "东湖新技术开发区", "一路", "光谷", "园", "栋", "地铁", "号", "北站", "坐", "拥", "独栋", "办公楼", "环境优美", "办公", "和谐", "交通", "便利", "地铁站", "有轨电车", "公交站", "交通工具",
        "齐全", "凯", "默", "电气", "期待", "加入", "积极参与", "依据", "工程", "跟进", "推动", "风险意识", "owner", "保持", "积极性", "自", "研", "内", "岗", "体验", "系统维护", "可能", "在线", "沟通交流", "简洁", "清晰", "录取", "优异者", "适当", "放宽", "上浮",
        "必要", "后期", "软件技术", "形成", "技术成果", "调研", "分析师", "专", "含", "信息管理", "跨专业", "从业人员", "注", "安排", "交代", "书写", "做事", "细心", "好学", "可以", "公休", "年终奖金", "定期", "正规", "养老", "医疗", "生育", "工伤", "失业", "关怀",
        "传统", "佳节", "之际", "礼包", "团结友爱", "伙伴", "丰富多彩", "两年", "过", "连接池", "划分", "检查", "部分", "甚至", "拆解", "硕士", "年龄", "周岁", "以下", "深厚", "语法", "浓厚", "优良", "治理", "a", "力", "高级", "能看懂", "有效", "共同", "想法", "提出",
        "意见", "前", "最", "重要", "企业", "极好", "驻场", "并且", "表单", "交互方式", "样式", "前端开发", "遵循", "开发进度", "实战经验", "其中", "强烈", "三维", "多个", "net", "对应", "数学", "理工科", "背景", "软件设计", "模式", "方法", "动手", "按", "质", "软件产品",
        "严格执行", "传", "帮", "带", "任务分配", "进度", "阶段", "介入", "本科学历", "五年", "尤佳", "比较", "细致", "态度", "享", "国家", "上班时间", "基本工资", "有关", "社会保险", "公司员工", "连续", "达到", "年限", "婚假", "产假", "护理", "发展潜力", "职员", "外出",
        "做好", "效率", "沉淀", "网络服务", "数据分析", "查询", "规范化", "标准化", "思考", "手", "款", "成功", "卡", "牌", "slg", "更佳", "可用性", "新人", "预研", "突破", "lambda", "理念", "它", "rest", "一个", "趋势", "思路", "影响", "医疗系统", "具体", "架构师",
        "保证系统", "大专", "三年", "体系", "写", "医院", "遇到", "验证", "运", "保障", "基本操作", "独立思考", "技术手段", "熟知", "懂", "应用环境", "表达能力", "个人", "新能源", "汽车", "权限", "排班", "绩效", "考勤", "知识库", "全局", "搜索", "门店", "渠道", "选址",
        "所有", "长远", "眼光", "局限于", "逻辑", "侧", "更好", "解决方案", "针对", "建模", "定位系统", "高质", "把", "控", "攻克", "t", "必须", "组件", "基本原理", "上进心", "驱动", "适应能力", "自信", "追求", "卓越", "感兴趣", "站", "角度", "思考问题", "tob", "商业化",
        "售后", "毕业", "通信", "数种", "优选", "it", "课堂", "所学", "在校", "期间", "校内外", "大赛", "参", "社区", "招聘", "类库", "优等", "b", "s", "方面", "海量", "数据系统", "测试工具", "曾", "主要", "爱好", "欢迎", "洁癖", "人士", "银行", "财务", "城市", "类产品", "实施",
        "保障系统", "健壮性", "可读性", "rpd", "原型", "联调", "准确无误", "系统优化", "技术标准", "总体设计", "文件", "整理", "功能设计", "技术类", "写作能力", "尤其", "套件", "公安", "细分", "增加", "bug", "电子", "swing", "桌面", "认证", "台", "检测", "安全隐患", "及时发现",
        "修补", "上级领导", "交办", "其它", "面向对象分析", "思想", "乐于助人", "全", "栈", "共享", "经济", "信", "主管", "下达", "执行力", "技巧", "试用期", "个", "月", "适应", "快", "随时", "表现", "\u003d", "到手", "工资", "享有", "提成", "超额", "业绩", "封顶", "足够", "发展前景",
        "发挥", "处", "高速", "发展期", "敢", "就", "元旦", "春节", "清明", "端午", "五一", "中秋", "国庆", "婚", "病假", "商品", "导购", "增长", "互动", "营销", "面对", "不断创新", "规模化", "上下游", "各", "域", "最终", "完整", "梳理", "链路", "关键", "点", "给出", "策略", "从业", "且",
        "可维护性", "不仅", "短期", "更", "方向", "不错", "交互", "主动", "应急", "组长", "tl", "加", "分", "一群", "怎样", "很", "热情", "喜欢", "敬畏", "心", "坚持", "主义", "持之以恒", "自己", "收获", "重视", "每", "一位", "主观", "能动性", "同学", "给予", "为此", "求贤若渴", "干货", "满满",
        "战斗", "大胆", "互相", "信任", "互相帮助", "生活", "里", "嗨", "皮", "徒步", "桌", "轰", "趴", "聚餐", "应有尽有"
    ]

    static numberRegex = /^[0-9]+$/

    static splitChar = " "

    static participleUrl = "https://www.tl.beer/api/v1/fenci"

    static participle(text) {
        return new Promise((resolve, reject) => {

            TampermonkeyApi.GMXmlHttpRequest({
                method: 'POST',
                timeout: 5000,
                url: JobWordCloud.participleUrl,

                data: "cont=" + encodeURIComponent(text) + "&cixin=false&model=false",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
                },
                onload: function (response) {
                    if (response.status !== 200) {
                        logger.error("分词状态码不是200", response.responseText)
                        return reject(response.responseText)
                    }
                    return resolve(JSON.parse(response.responseText).data.split(JobWordCloud.splitChar))
                },
                onerror: function (error) {
                    logger.error("分词出错", error)
                    reject(error)
                }
            });
        })
    }

    static buildWord(wordArr) {
        // {"word1":1, "word2":4}
        let weightMap = {};
        for (let i = 0; i < wordArr.length; i++) {
            let str = wordArr[i];
            if (JobWordCloud.filterableWorldArr.includes(str)) {
                continue;
            }
            if (JobWordCloud.numberRegex.test(str)) {
                continue;
            }
            if (str in weightMap) {
                weightMap[str] = weightMap[str] + 1;
                continue
            }
            weightMap[str] = 1;
        }

        // 将对象转换为二维数组并排序： [['word1', 2], ['word2', 4]]
        let weightWordArr = JobWordCloud.sortByValue(Object.entries(weightMap));
        return JobWordCloud.cutData(weightWordArr)
    }

    static cutData(weightWordArr) {
        return weightWordArr
    }

    static generateWorldCloudImage(canvasTagId, weightWordArr) {
        // 词云图的配置选项
        let options = {
            tooltip: {
                show: true,
                formatter: function (item) {
                    return item[0] + ': ' + item[1]
                }
            },
            list: weightWordArr,
            // 网格尺寸
            //gridSize: 10,
            // 权重系数
            weightFactor: 2,
            // 字体
            fontFamily: 'Finger Paint, cursive, sans-serif',
            // 字体颜色，也可以指定特定颜色值
            //color: '#26ad7e',
            color: 'random-dark',
            // 旋转比例
            // rotateRatio: 0.2,
            // 背景颜色
            backgroundColor: 'white',
            // 形状
            //shape: 'square',
            shape: 'circle',
            ellipticity: 1,
            // 随机排列词语
            shuffle: true,
            // 不绘制超出容器边界的词语
            drawOutOfBound: false
        };

        // WordCloud(document.getElementById(canvasTagId), options);
        const wc = new Js2WordCloud(document.getElementById(canvasTagId));
        wc.setOption(options)
    }

    static getKeyWorldArr(twoArr) {
        let worldArr = []
        for (let i = 0; i < twoArr.length; i++) {
            let world = twoArr[i][0];
            worldArr.push(world)
        }
        return worldArr;
    }

    static sortByValue(arr, order = 'desc') {
        if (order === 'asc') {
            return arr.sort((a, b) => a[1] - b[1]);
        } else if (order === 'desc') {
            return arr.sort((a, b) => b[1] - a[1]);
        } else {
            throw new Error('Invalid sort key. Use "asc" or "desc".');
        }
    }

}


GM_registerMenuCommand("切换Ck", async () => {
    let value = GM_getValue("ck_list") || [];
    GM_cookie("list", {}, async (list, error) => {
        if (error === undefined) {
            console.log(list, value);
            // 储存覆盖老的值
            GM_setValue("ck_list", list);
            // 先清空 再设置
            for (let i = 0; i < list.length; i++) {
                list[i].url = window.location.origin;
                await GM_cookie("delete", list[i]);
            }
            if (value.length) {
                // 循环set
                for (let i = 0; i < value.length; i++) {
                    value[i].url = window.location.origin;
                    await GM_cookie("set", value[i]);
                }
            }
            if (GM_getValue("ck_cur", "") === "") {
                GM_setValue("ck_cur", "_");
            } else {
                GM_setValue("ck_cur", "");
            }
            window.location.reload();
            // window.alert("手动刷新～");
        } else {
            window.alert("你当前版本可能不支持Ck操作，错误代码：" + error);
        }
    });
});

GM_registerMenuCommand("清除当前Ck", () => {
    if (GM_getValue("ck_cur", "") === "_") {
        GM_setValue("ck_cur", "");
    }
    GM_cookie("list", {}, async (list, error) => {
        if (error === undefined) {
            // 清空
            for (let i = 0; i < list.length; i++) {
                list[i].url = window.location.origin;
                // console.log(list[i]);
                await GM_cookie("delete", list[i]);
            }

            window.location.reload();
        } else {
            window.alert("你当前版本可能不支持Ck操作，错误代码：" + error);
        }
    });
});

GM_registerMenuCommand("清空所有存储!", async () => {
    if (confirm("将清空脚本全部的设置!!")) {
        const asyncKeys = await GM_listValues();
        for (let index in asyncKeys) {
            if (!asyncKeys.hasOwnProperty(index)) {
                continue;
            }
            console.log(asyncKeys[index]);
            await GM_deleteValue(asyncKeys[index]);
        }
        window.alert("OK!");
    }
});

(function () {
    const list_url = "web/geek/job";
    const recommend_url = "web/geek/recommend";
    const message_url = "web/geek/chat";

    if (document.URL.includes(list_url) || document.URL.includes(recommend_url)) {
        window.addEventListener("load", () => {
            new JobListPageHandler()
        });
    } else if (document.URL.includes(message_url) && parent?.document?.getElementById('msgIframe')) {
        window.addEventListener("load", () => {
            // jobListPage内部的 msgIframe才注册
            new JobMessagePageHandler();
        });
    }
})();
